在实战篇中,我们对请求策略及缓存置换策略进行了讨论,通过这两个机制我们能够高效地利用本地缓存来提高应用性能及可用性。本章我们将学习这两个机制在 Workbox 中的使用。

# 请求策略

在实战篇:请求策略中,我们讨论了五种常见的请求策略,分别为:

  • 缓存优先:首先从缓存中进行匹配,如果存在相关请求的响应,返回该响应,否则通过网络获取。
  • 网络优先:首先通过网络获取,如果请求异常,则从缓存中获取。
  • 仅使用缓存:所有请求都从缓存中获取。
  • 仅使用网络:所有请求都从网络中获取。
  • 先缓存后网络:缓存优先的升级版,它与后者的唯一区别是,如果在缓存中匹配到相关请求的响应,在返回该响应的同时依旧会发起网络请求,并更新相关缓存。

Workbox 中的 workbox-strategies 模块为我们实现了上述常见策略,相关类分别为:

  • 缓存优先:workbox.strategies.CacheFirst
  • 网络优先:workbox.strategies.NetworkFirst
  • 仅使用缓存:workbox.strategies.CacheOnly
  • 仅使用网络:workbox.strategies.NetworkOnly
  • 先缓存后网络:workbox.strategies.StaleWhileRevalidate

这些类的构造函数皆接收含有以下属性的对象:

  • cacheName:缓存名称,默认值为 Workbox 配置中的运行时缓存名。
  • plugins: 插件数组列表,在获取或缓存请求时会调用它们的生命周期方法以便执行一些额外操作(比如清空过期缓存)。
  • fetchOptions:网络请求配置信息,结构与函数 fetch 中的 init 参数一致(在 CacheOnly 中,该属性将会被忽略)。
  • matchOptionsCacheQueryOptions 对象(在 NetworkOnly 中,该属性将会被忽略)。
  • networkTimeoutSeconds:如果对该属性进行了赋值,那么网络会在指定的时间内没有响应时使用本地缓存来进行响应(该属性仅在 NetworkFirst 中有效)

上文对 Workbox 中常见策略的实现进行了简单介绍,接下来我们来看一下它具体的使用,比如:

workbox.routing.registerRoute(
  '/api/users',
  new workbox.strategies.NetworkFirst({...})
);

上例中,我们使用了上一章介绍的 workbox.routing.registerRoute 方法来拦截请求 /api/users,并通过 workbox.strategies.NetworkFirst 的实例进行响应,结合上一章的学习,我们可以确定上述五个类中必定包含实例方法 handle,故可据此实现自己的请求策略,比如:

class CustomStrategy {
  async handle({ url, event, request, params }) {
    new Response(...);
  }
}

workbox.routing.registerRoute(
  '/api/users',
  new CustomStrategy()
);

除了将请求策略类的实例作为 workbox.routing.registerRoute 方法的 handler 参数外,我们也可以在自定义的 fetch 事件中直接使用,比如:

self.addEventListener('fetch', (event) => {
  const { pathname } = new URL(event.request.url, location);
  if (pathname === '/api/users') {
    const networkFirst = new workbox.strategies.NetworkFirst();
    event.respondWith(networkFirst.handle({ event }));
  }
});

# 缓存置换

在实战篇:缓存置换策略中,我们讨论了三种常见的缓存置换控制算法,分别为:

  • FIFO:先进先出算法,当缓存空间不足时,优先删除最先加入缓存的数据项。
  • LRU:最近最少使用算法,当缓存空间不足时,优先删除最久没有被访问到的数据。
  • LFU:最不常使用算法,当缓存空间不足时,优先删除访问次数较少的数据。

Workbox 为我们提供了基于 FIFO 算法的缓存置换控制插件:workbox.expiration.Plugin,基本使用如下:

workbox.routing.registerRoute(
  '/api/users',
  new workbox.strategies.NetworkFirst({
    cacheName: 'expiration-cache',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 20,
        maxAgeSeconds: 24 * 60 * 60 // 1 day
      })
    ]
  })
);

上例中,我们将 workbox.expiration.Plugin 的实例赋值给 workbox.strategies.NetworkFirst 构造参数中的 plugins 属性,然后在读取或更新缓存时,该插件将自动调用,并按照 FIFO算法来清理过期的缓存条目。其参数属性为:

  • maxEntries:指定缓存名称下(此处为 expiration-cache)最多可存储的缓存条目数量。
  • maxAgeSeconds:请求被添加到缓存之后的有效期,单位为毫秒。

workbox.expiration.Plugin 的使用非常简单,唯一需要注意的是:

  • 必须指定 cacheName 且值不能与 Workbox 运行时缓存名(通过 workbox.core.cacheNames.runtime 获得)相同,否则将抛出 expire-custom-caches-only 异常。
  • 由于 IndexedDB 的执行速度较慢,如果一个缓存已经过期,它很可能不会被立即清除,此时应用得到的是已过期的缓存,故为了避免此类情况的发生,我们在使用 workbox.expiration.Plugin 时,尽量指定 maxAgeSeconds 的值。

同理,我们可通过 workbox.expiration.CacheExpiration 在自定义的 fetch 事件中处理过期缓存,比如:

const cacheName = 'expiration-cache';
const expirationManager = new workbox.expiration.CacheExpiration(
  cacheName,
  {
    maxEntries: 20,
    maxAgeSeconds: 24 * 60 * 60
  }
);

代码中,我们创建了 workbox.expiration.CacheExpiration 的实例 expirationManager,然后便可在读取或更新缓存的时候调用以下方法来清理过期缓存:

await expirationManager.expireEntries();

在缓存更新时调用以下方法来更新相关缓存的有效时间:

await expirationManager.updateTimestamp(request.url);

# 总结

本章我们对 Workbox 中的请求策略、缓存置换的使用及注意事项进行了讲解,下一章,我们将讨论缓存相关的下一个主题:导航预加载在 Workbox 中的使用

阅读全文